/*
 *   Copyright 2008 Francois Fernandes
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 */
package com.googlecode.opendoclibrary.internal.impl.fs;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.DigestOutputStream;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;

import com.googlecode.opendoclibrary.internal.impl.fs.meta.Metadata;
import com.googlecode.opendoclibrary.library.spi.IDocumentId;
import com.googlecode.opendoclibrary.library.spi.ILibrary;
import com.googlecode.opendoclibrary.library.spi.IMetadata;
import com.googlecode.opendoclibrary.library.spi.IMetadataView;
import com.googlecode.opendoclibrary.library.spi.IStoredData;
import com.googlecode.opendoclibrary.library.spi.LibraryException;
import com.googlecode.opendoclibrary.library.spi.LibraryVerificationException;
import com.googlecode.opendoclibrary.server.IDataHandle;

public class FilesystemLibrary implements ILibrary, IFilesystemLibrary
{

    private static final byte[] FALSE_BYTES = { 'f', 'a', 'l', 's', 'e' };

    private static final String METADATA_SUFFIX = ".metadata";

    private static final String PUBLISHED_SUFFIX = ".published";

    private static final byte[] TRUE_BYTES = { 't', 'r', 'u', 'e' };

    private File libraryData;

    private File libraryHome;

    private File libraryMetadata;

    private boolean arraysEqual( byte[] b1, int off1, byte[] b2, int off2, int len )
    {
        if ( off1 + len >= b1.length )
        {
            return false;
        }

        if ( off2 + len >= b2.length )
        {
            return false;
        }

        for ( int i = 0; i < len; i++ )
        {
            if ( b1[off1 + i] != b2[off2 + i] )
            {
                return false;
            }
        }
        return true;
    }

    @Override
    public OutputStream createDocumentDataOutput( IDocumentId id ) throws LibraryException
    {
        // FIXME do some more checking

        try
        {
            File file = getDocumentContentFile( id.toString() );
            if ( file.exists() )
            {
                throw new IllegalArgumentException( "document data with id: " + id.asString() + " already exists" );
            }
            return new FileOutputStream( file );
        }
        catch ( FileNotFoundException e )
        {
            throw new LibraryException( "unable to create document data file", e );
        }
    }

    @Override
    public IMetadata createMetadata( IDocumentId documentId )
    {
        return new Metadata( this, documentId );
    }

    @Override
    public OutputStream createMetadataOutput( IDocumentId documentId ) throws LibraryException
    {
        try
        {
            return new FileOutputStream( getMetadataFile( documentId ) );
        }
        catch ( FileNotFoundException e )
        {
            throw new LibraryException( "unable to write meta data", e );
        }
    }

    private void createRequiredDirectory( File dir ) throws LibraryVerificationException
    {
        if ( !dir.mkdirs() )
        {
            throw new LibraryVerificationException( "Unable to initialize library. "
                            + "Can't create required directory: " + dir );
        }
    }

    private void createRequiredFile( File file, String initialContent, String encoding )
        throws LibraryVerificationException
    {
        try
        {
            FileOutputStream fout = new FileOutputStream( file );
            fout.write( initialContent.getBytes( encoding ) );
            fout.flush();
            fout.close();
        }
        catch ( Exception e )
        {
            throw new LibraryVerificationException( "Unable to initialize library. "
                            + "Creation of required file failed: " + file );
        }

    }

    private MessageDigest requireDigest( String algName ) throws LibraryException
    {
        MessageDigest md5Digest;
        try
        {
            md5Digest = MessageDigest.getInstance( algName );
        }
        catch ( NoSuchAlgorithmException e )
        {
            throw new LibraryException( "missing digesting algorith:" + algName + ". Document storage not possible." );
        }
        return md5Digest;
    }

    @Override
    public void store( IDataHandle data, IDocumentId id ) throws LibraryException
    {
        long storageInitiation = System.currentTimeMillis();

        IMetadata meta = createMetadata( id );

        // update odl metadata
        IMetadataView odlMeta = meta.getView( IMetadata.ODL_INTERNAL_PREFIX );
        SimpleDateFormat dateFormat = new SimpleDateFormat( "yyyy-MM-dd'T'HH:mm:ss.SSSZ" );

        odlMeta.putString( "insertion-date", dateFormat.format( new Date( storageInitiation ) ) );

        IMetadataView fsLibMeta = meta.getView( IFilesystemLibrary.FS_LIBRARY_NS_PREFIX );
        OutputStream out = createDocumentDataOutput( id );

        MessageDigest md5Digest = requireDigest( "MD5" );
        MessageDigest sha1Digest = requireDigest( "SHA1" );

        try
        {
            DigestOutputStream digestOut =
                new DigestOutputStream( new DigestOutputStream( out, md5Digest ), sha1Digest );

            InputStream is = data.asInputStream();
            copy( is, digestOut );
            digestOut.flush();
            out.flush();
            out.close();
        }
        catch ( IOException e )
        {
            throw new LibraryException( "Writing the document data failed.", e );
        }

        fsLibMeta.putString( IFilesystemLibrary.META_KEY_DOC_MD5, toHexString( md5Digest.digest() ) );
        fsLibMeta.putString( IFilesystemLibrary.META_KEY_DOC_SHA1, toHexString( sha1Digest.digest() ) );

        meta.save();

        // everything is done now. Tell the library to publish the document and make access to it possible
        publish( id );

    }

    private void copy( InputStream is, OutputStream digestOut ) throws IOException
    {
        byte[] buf = new byte[1024];
        int read;
        while ( ( read = is.read( buf ) ) >= 0 )
        {
            digestOut.write( buf, 0, read );
        }
    }

    public FilesystemLibrary( File libraryHome )
    {
        this.libraryHome = libraryHome;
        libraryMetadata = new File( libraryHome, ".meta" );
        libraryData = new File( libraryHome, "data" );
    }

    private static final char[] HEX_DIGITS =
        { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', };

    private static final String toHexString( byte[] bytes )
    {
        StringBuffer buf = new StringBuffer();
        for ( byte b : bytes )
        {
            buf.append( HEX_DIGITS[( b >>> 4 ) & 0x0f] );
            buf.append( HEX_DIGITS[b & 0x0f] );
        }
        return buf.toString();
    }

    private void ensureRequiredDirectoryExists( File dir ) throws LibraryVerificationException
    {
        if ( !dir.isDirectory() )
        {
            throw new LibraryVerificationException( "Unable to initialize library. "
                            + "Required directory does not exist: " + dir );
        }
    }

    private void ensureRequiredFileExists( File file ) throws LibraryVerificationException
    {
        if ( !file.isFile() )
        {
            throw new LibraryVerificationException( "Unable to initialize library. " + "Required file does not exist: "
                            + file );
        }
    }

    private File getDocumentContentFile( String id )
    {
        return new File( libraryData, id );
    }

    private File getMetadataFile( IDocumentId docId )
    {
        File docContent = getDocumentContentFile( docId.toString() );
        return new File( docContent.getParentFile(), docContent.getName() + METADATA_SUFFIX );
    }

    private File getPublishStateFile( File file )
    {
        return new File( file.getParentFile(), file.getName() + PUBLISHED_SUFFIX );
    }

    @Override
    public void prepareShutdown()
    {
    }

    @Override
    public void publish( IDocumentId docId ) throws LibraryException
    {

        File documentDataFile = getDocumentContentFile( docId.toString() );
        if ( !documentDataFile.exists() || !documentDataFile.isFile() )
        {
            throw new IllegalArgumentException( "not a valid document id" );
        }
        try
        {
            File publishedFile = getPublishStateFile( documentDataFile );
            if ( publishedFile.exists() )
            {
                boolean b = readPublishedState( publishedFile );
                if ( b )
                {
                    throw new LibraryException( "file is already published" );
                }
            }
            FileOutputStream out = new FileOutputStream( publishedFile );
            out.write( "true".getBytes( "ascii" ) );
            out.flush();
            out.close();
        }
        catch ( Exception e )
        {
            throw new LibraryException( "unable to publish document with id: " + docId, e );
        }
    }

    private boolean readPublishedState( File publishedFile ) throws IOException, LibraryException
    {
        FileInputStream in = new FileInputStream( publishedFile );
        byte[] buf = new byte[5]; // 5: max len of false
        int read = 0;
        int off = 0;
        while ( ( read = in.read( buf, off, buf.length - off ) ) != -1 && ( off += read ) < buf.length )
        {
            ;
        }
        if ( in.read() != -1 || off != TRUE_BYTES.length || off != FALSE_BYTES.length )
        {
            throw new LibraryException( "published state file has unknown format" );
        }

        if ( off == TRUE_BYTES.length )
        {
            if ( !arraysEqual( TRUE_BYTES, 0, buf, 0, off ) )
            {
                throw new LibraryException( "published state file has unknown format" );
            }
            return true;
        }

        if ( !arraysEqual( FALSE_BYTES, 0, buf, 0, off ) )
        {
            throw new LibraryException( "published state file has unknown format" );
        }
        return false;
    }

    @Override
    public IStoredData retrieve( IDocumentId id ) throws LibraryException
    {
        File target = getDocumentContentFile( id.toString() );
        if ( !target.exists() )
        {
            throw new IllegalArgumentException( "no document wit id: " + id );
        }
        File publishStateFile = getPublishStateFile( target );
        try
        {
            if ( !publishStateFile.exists() || !readPublishedState( publishStateFile ) )
            {
                throw new IllegalStateException( "document with id: <" + id + "> is pending for publishing" );
            }
        }
        catch ( IOException e )
        {
            throw new LibraryException( "unable to read published state.", e );
        }

        return new StoredData( id, target );
    }

    @Override
    public void verifyLibraryIntegrity() throws LibraryVerificationException
    {
        if ( libraryHome.isDirectory() )
        {
            ensureRequiredDirectoryExists( libraryHome );
            ensureRequiredDirectoryExists( libraryMetadata );
            ensureRequiredDirectoryExists( libraryData );
        }
        else if ( !libraryHome.exists() )
        {
            createRequiredDirectory( libraryHome );
            createRequiredDirectory( libraryMetadata );
            createRequiredDirectory( libraryData );
        }
        else
        {
            // something strange has happened
            throw new LibraryVerificationException( "library home is not pointing to a valid directory: " + libraryHome );
        }

    }
}
